package org.eggermont.hm.util;

import java.util.ArrayList;
import java.util.List;

import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;

import cern.colt.matrix.DoubleFactory1D;
import cern.colt.matrix.DoubleMatrix1D;
import cern.jet.math.Functions;

/**
 * Representation of an n-dimensional bounding box.
 * 
 * @author michiel
 * 
 */
public class BoundingBox {

    private final List<Interval> intervals;

    public BoundingBox(final double[] a, final double[] b) {
        this(DoubleFactory1D.dense.make(a), DoubleFactory1D.dense.make(b));
    }

    public BoundingBox(final DoubleMatrix1D a, final DoubleMatrix1D b) {
        if (a == null || b == null) {
            throw new NullPointerException("");
        }
        if (a.size() != b.size()) {
            throw new IllegalArgumentException();
        }
        final DoubleMatrix1D lowerBound = a.copy().assign(b, Functions.min);
        final DoubleMatrix1D upperBound = a.copy().assign(b, Functions.max);
        intervals = new ArrayList<Interval>(a.size());
        for (int i = 0; i < a.size(); ++i) {
            intervals.add(new Interval(lowerBound.get(i), upperBound.get(i)));
        }
    }

    private BoundingBox(final List<Interval> intervals) {
        this.intervals = intervals;
    }

    public int dims() {
        return intervals.size();
    }

    public BoundingBox copy() {
        return new BoundingBox(getLowerBound(), getUpperBound());
    }

    public DoubleMatrix1D getLowerBound() {
        final DoubleMatrix1D lowerBound = DoubleFactory1D.dense.make(dims());
        for (int i = 0; i < dims(); ++i) {
            lowerBound.set(i, intervals.get(i).getLowerBound());
        }
        return lowerBound;
    }

    public DoubleMatrix1D getUpperBound() {
        final DoubleMatrix1D upperBound = DoubleFactory1D.dense.make(dims());
        for (int i = 0; i < dims(); ++i) {
            upperBound.set(i, intervals.get(i).getUpperBound());
        }
        return upperBound;
    }

    public DoubleMatrix1D eval(double fraction) {
        return getLowerBound().assign(getWidth(), Functions.plusMult(fraction));
    }

    public DoubleMatrix1D getWidth() {
        return getUpperBound().assign(getLowerBound(), Functions.minus);
    }

    public double getDiameter() {
        return getWidth().aggregate(Functions.plus, Functions.square);
    }

    public DoubleMatrix1D getCenter() {
        return eval(0.5);
    }

    public BoundingBox union(final BoundingBox other) {
        return new BoundingBox(getLowerBound().assign(other.getLowerBound(),
                Functions.min), getUpperBound().assign(other.getUpperBound(),
                Functions.max));
    }

    public DoubleMatrix1D dist(final BoundingBox other) {
        final DoubleMatrix1D dist = DoubleFactory1D.dense.make(dims());
        for (int i = 0; i < dims(); ++i) {
            dist.set(i, intervals.get(i).distance(other.intervals.get(i)));
        }
        return dist;
    }

    public double distance(final BoundingBox other) {
        final DoubleMatrix1D d = dist(other);
        return Math.sqrt(d.zDotProduct(d));
    }

    public DoubleMatrix1D dist(final DoubleMatrix1D x) {
        final DoubleMatrix1D dist = x.copy();
        for (int i = 0; i < dist.size(); ++i) {
            dist.set(i, intervals.get(i).distance(x.get(i)));
        }
        return dist;
    }

    public double distance(final DoubleMatrix1D x) {
        final DoubleMatrix1D d = dist(x);
        return Math.sqrt(d.zDotProduct(d));
    }

    public BoundingBox[] subdivide(double fraction) {
        if (!Functions.isBetween(0.0, 1.0).apply(fraction)) {
            throw new RuntimeException("fraction not between 0 and 1");
        }
        final List<Interval> lowerIntervals = new ArrayList<Interval>(dims());
        final List<Interval> upperIntervals = new ArrayList<Interval>(dims());
        for (Interval i : intervals) {
            Interval[] subs = i.subdivide(fraction);
            lowerIntervals.add(subs[0]);
            upperIntervals.add(subs[1]);
        }
        return new BoundingBox[] { new BoundingBox(lowerIntervals),
                new BoundingBox(upperIntervals) };
    }

    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if ((o == null) || (!getClass().isAssignableFrom(o.getClass()))) {
            return false;
        }
        final BoundingBox other = (BoundingBox) o;
        if (dims() != other.dims()) {
            return false;
        }
        return new EqualsBuilder().append(intervals, other.intervals)
                .isEquals();
    }

    public int hashCode() {
        return new HashCodeBuilder(13, 99).append(intervals).toHashCode();
    }

    public String toString() {
        final StringBuilder builder = new StringBuilder();
        for (Interval i : intervals) {
            builder.append(i).append(", ");
        }
        return builder.substring(0, builder.length() - 2);
    }
}
